1 module geany_dlang.plugin;
2 
3 import geany_d_binding.geany.plugins;
4 import geany_d_binding.geany.types;
5 import geany_dlang.dcd_wrapper;
6 import geany_dlang.config: ConfigFile;
7 import logger;
8 import geany_d_binding.geany.sciwrappers;
9 import std.conv: to;
10 import dcd.common.messages;
11 import geany_d_binding.geany.document;
12 import geany_d_binding.geany.filetypes;
13 import geany_d_binding.scintilla.types;
14 import geany_d_binding.scintilla.Scintilla;
15 import geany_d_binding.scintilla.ScintillaGTK;
16 import std.string: toStringz;
17 
18 private GeanyPlugin* geany_plugin;
19 package DcdWrapper wrapper;
20 ConfigFile configFile;
21 
22 void init_keybindings() nothrow
23 {
24     import geany_d_binding.geany.pluginutils;
25     import geany_d_binding.geany.keybindings;
26     import gdk.Gdk: GdkModifierType;
27     import gtk.Widget: GtkWidget;
28 
29     const gsize COUNT_KB = 2;
30 
31     GeanyKeyGroup* key_group = plugin_set_key_group(
32             geany_plugin,
33             "dlang_keys",
34             COUNT_KB,
35             null // GeanyKeyGroupCallback
36         );
37 
38     const gint KB_COMPLETE_IDX = 0;
39     const guint KEY = 0;
40 
41     keybindings_set_item(
42             key_group,
43             KB_COMPLETE_IDX,
44             &force_completion,
45             KEY,
46             cast(GdkModifierType) 0,
47             "exec",
48             "Display autocompletion dialog",
49             null // GtkWidget*
50         );
51 
52     keybindings_set_item(
53             key_group,
54             1,
55             &show_debug,
56             KEY,
57             cast(GdkModifierType) 0,
58             "exec 2",
59             "Dump debug info into log",
60             null // GtkWidget*
61         );
62 }
63 
64 void addCurrDocumentDirIntoImport(GeanyDocument* doc) nothrow
65 {
66     nothrowLog!"trace"(__FUNCTION__);
67 
68     if(doc != null && doc.file_type.id == GeanyFiletypeID.GEANY_FILETYPES_D)
69     {
70         import std.path;
71 
72         string filename = doc.file_name.to!string;
73         string path = dirName(filename);
74 
75         try
76         {
77             nothrowLog!"trace"("Begin adding import path "~path);
78 
79             wrapper.addImportPaths([path.to!string]);
80         }
81         catch(Exception e)
82             nothrowLog!"warning"(e.msg);
83     }
84 }
85 
86 void attemptDisplaySomeWindow() nothrow
87 {
88     GeanyDocument* doc = document_get_current();
89     const res = calculateCompletion(doc);
90 
91     if(res.completions.length > 0)
92     {
93         const currPos = doc.editor.sci.sci_get_current_position;
94 
95         const separator = cast(char) scintilla_send_message(
96                 doc.editor.sci,
97                 Sci.SCI_AUTOCGETSEPARATOR,
98                 null,
99                 null
100             );
101 
102         with(CompletionType)
103         switch(res.completionType)
104         {
105             case identifiers:
106                 attemptDisplayCompletionWindow(doc, res, separator, currPos);
107                 break;
108 
109             case calltips:
110                 attemptDisplayTipWindow(doc, res, separator, currPos);
111                 break;
112 
113             default:
114                 nothrowLog!"warning"("Unknown completionType "~res.completionType.to!string);
115                 break;
116         }
117     }
118 }
119 
120 void attemptDisplayCompletionWindow(GeanyDocument* doc, in AutocompleteResponse res, char separator, int currPos) nothrow
121 {
122     const wordStartPos = cast(size_t) scintilla_send_message(
123             doc.editor.sci,
124             Sci.SCI_WORDSTARTPOSITION,
125             cast(uptr_t) currPos,
126             cast(sptr_t) true
127         );
128 
129     string preparedList;
130 
131     foreach(i, ref c; res.completions)
132     {
133         if(i != 0)
134             preparedList ~= separator;
135 
136         preparedList ~= c.identifier;
137 
138         switch(c.kind)
139         {
140             case 'k':
141                 preparedList ~= "?1";
142                 break;
143 
144             case 'v':
145                 preparedList ~= "?2";
146                 break;
147 
148             default:
149                 break;
150         }
151     }
152 
153     scintilla_send_message(
154             doc.editor.sci,
155             Sci.SCI_AUTOCSETORDER,
156             cast(uptr_t) Sc.SC_ORDER_PERFORMSORT,
157             null
158         );
159 
160     const size_t alreadyEnteredNum = currPos - wordStartPos;
161     nothrowLog!"trace"("alreadyEnteredNum="~alreadyEnteredNum.to!string);
162 
163     scintilla_send_message(
164             doc.editor.sci,
165             Sci.SCI_AUTOCSHOW,
166             cast(uptr_t) alreadyEnteredNum,
167             cast(sptr_t) preparedList.toStringz
168         );
169 }
170 
171 void attemptDisplayTipWindow(GeanyDocument* doc, in AutocompleteResponse tips, char separator, int pos) nothrow
172 {
173     string str;
174 
175     foreach(i, ref c; tips.completions)
176     {
177         if(i != 0)
178             str ~= separator;
179 
180         str ~= c.definition;
181     }
182 
183     scintilla_send_message(
184             doc.editor.sci,
185             Sci.SCI_CALLTIPSHOW,
186             cast(uptr_t) pos,
187             cast(sptr_t) str.toStringz
188         );
189 }
190 
191 AutocompleteResponse calculateCompletion(GeanyDocument* doc) nothrow
192 {
193     AutocompleteResponse ret;
194 
195     if(doc != null && doc.file_type.id == GeanyFiletypeID.GEANY_FILETYPES_D)
196     {
197         auto sci = doc.editor.sci;
198         const textLen = sci.sci_get_length;
199         char* textBuff = sci.sci_get_contents(-1);
200         scope(exit) g_free(textBuff);
201 
202         AutocompleteRequest req;
203         req.kind = RequestKind.autocomplete;
204         req.cursorPosition = sci.sci_get_current_position;
205         req.sourceCode = cast(ubyte[]) textBuff[0 .. textLen+1];
206 
207         ret = wrapper.doRequest(req);
208     }
209 
210     return ret;
211 }
212 
213 package void substituteDcdPaths(ref DcdWrapper wrapper, string[] paths)
214 {
215     wrapper.cache.clear();
216     wrapper.addImportPaths = paths;
217 }
218 
219 extern(System) nothrow:
220 
221 import gtkc.gobjecttypes: GObject;
222 import geany_d_binding.geany.editor: GeanyEditor;
223 import geany_d_binding.scintilla.Scintilla: SCNotification, Msg;
224 
225 gboolean on_editor_notify(GObject* object, GeanyEditor* editor, SCNotification* nt, gpointer data)
226 {
227     return configFile.config.useCharAddEvent ?
228         processEditorNotify(object, nt) : false;
229 }
230 
231 private gboolean processEditorNotify(GObject* object, SCNotification* nt)
232 {
233     import geany_d_binding.geany.dialogs;
234     import gtkc.gtktypes: GtkMessageType;
235 
236     with(Msg)
237     switch (nt.nmhdr.code)
238     {
239         case SCN_CHARADDED:
240             nothrowLog!"trace"("SCN_CHARADDED received");
241             attemptDisplaySomeWindow();
242             return true;
243 
244         case SCN_KEY:
245         case SCN_UPDATEUI:
246         case SCN_MODIFIED:
247         case SCN_PAINTED:
248         case SCN_FOCUSIN:
249         case SCN_FOCUSOUT:
250         case SCN_ZOOM:
251             break;
252 
253         default:
254             auto c = cast(Msg) nt.nmhdr.code;
255             nothrowLog!"trace"("Notify code="~c.to!string);
256             break;
257     }
258 
259     return false;
260 }
261 
262 void on_document_filetype_set(GObject* obj, GeanyDocument* doc, GeanyFiletype* filetype_old, gpointer user_data)
263 {
264     nothrowLog!"trace"(__FUNCTION__);
265 
266     addCurrDocumentDirIntoImport(doc);
267 }
268 
269 void show_debug(guint key_id)
270 {
271     nothrowLog!"info"(
272         "Import paths:\n"~wrapper.listImportPaths.to!string
273     );
274 
275     nothrowLog!"info"(wrapper.cache.symbolsAllocated.to!string~" symbols cached.");
276 }
277 
278 void force_completion(guint key_id)
279 {
280     attemptDisplaySomeWindow();
281 }
282 
283 gboolean initPlugin(GeanyPlugin *plugin, gpointer pdata)
284 {
285     nothrowLog!"trace"(__FUNCTION__);
286 
287     geany_plugin = plugin;
288 
289     try
290     {
291         configFile = new ConfigFile(plugin.geany_data);
292 
293         wrapper = new DcdWrapper();
294         wrapper.substituteDcdPaths(configFile.config.additionalPaths);
295     }
296     catch(Exception e)
297     {
298         nothrowLog!"fatal"(e.msg);
299 
300         return false;
301     }
302 
303     init_keybindings();
304 
305     return true;
306 }
307 
308 void cleanupPlugin(GeanyPlugin* plugin, gpointer pdata)
309 {
310     try
311     {
312         destroy(wrapper);
313         destroy(configFile);
314     }
315     catch(Exception e)
316         nothrowLog!"fatal"(e.msg);
317 }
318 
319 private PluginCallback[] callbacks;
320 
321 shared static this()
322 {
323     import gtkc.gobjecttypes: GCallback;
324 
325     callbacks =
326     [
327         PluginCallback("editor-notify", cast(GCallback) &on_editor_notify, false, null),
328         PluginCallback("document-filetype-set", cast(GCallback) &on_document_filetype_set, true, null),
329         PluginCallback(null, null, false, null)
330     ];
331 }
332 
333 void geany_load_module(GeanyPlugin *plugin)
334 {
335     import geany_dlang.config_window: configWindowDialog;
336 
337     plugin.funcs._init = &initPlugin;
338     plugin.funcs.cleanup = &cleanupPlugin;
339     plugin.funcs.callbacks = &callbacks[0];
340     plugin.funcs.configure = &configWindowDialog;
341 
342     plugin.info.name = "D language";
343     plugin.info.description = "Adds D language support";
344     plugin.info._version = "0.x.x"; //TODO: fill out automatically
345     plugin.info.author = "Denis Feklushkin <denis.feklushkin@gmail.com>";
346 
347     try
348         GEANY_PLUGIN_REGISTER(plugin, 225);
349     catch(Exception e)
350         nothrowLog!"fatal"(e.msg);
351 }